<?php

/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Ldap
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Ldap.php 18881 2009-11-06 10:55:19Z sgehrig $
 */

/**
 * @category   Zend
 * @package    Zend_Ldap
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Ldap
{
	const SEARCH_SCOPE_SUB  = 1;
	const SEARCH_SCOPE_ONE  = 2;
	const SEARCH_SCOPE_BASE = 3;

	const ACCTNAME_FORM_DN        = 1;
	const ACCTNAME_FORM_USERNAME  = 2;
	const ACCTNAME_FORM_BACKSLASH = 3;
	const ACCTNAME_FORM_PRINCIPAL = 4;

	/**
	 * String used with ldap_connect for error handling purposes.
	 *
	 * @var string
	 */
	private $_connectString;

	/**
	 * The options used in connecting, binding, etc.
	 *
	 * @var array
	 */
	protected $_options = null;

	/**
	 * The raw LDAP extension resource.
	 *
	 * @var resource
	 */
	protected $_resource = null;

	/**
	 * Caches the RootDSE
	 *
	 * @var Zend_Ldap_Node
	 */
	protected $_rootDse = null;

	/**
	 * Caches the schema
	 *
	 * @var Zend_Ldap_Node
	 */
	protected $_schema = null;

	/**
	 * @deprecated will be removed, use {@see Zend_Ldap_Filter_Abstract::escapeValue()}
	 * @param  string $str The string to escape.
	 * @return string The escaped string
	 */
	public static function filterEscape($str)
	{
		/**
		 * @see Zend_Ldap_Filter_Abstract
		 */
		require_once 'Zend/Ldap/Filter/Abstract.php';
		return Zend_Ldap_Filter_Abstract::escapeValue($str);
	}

	/**
	 * @deprecated will be removed, use {@see Zend_Ldap_Dn::checkDn()}
	 * @param  string $dn   The DN to parse
	 * @param  array  $keys An optional array to receive DN keys (e.g. CN, OU, DC, ...)
	 * @param  array  $vals An optional array to receive DN values
	 * @return boolean True if the DN was successfully parsed or false if the string is
	 * not a valid DN.
	 */
	public static function explodeDn($dn, array &$keys = null, array &$vals = null)
	{
		/**
		 * @see Zend_Ldap_Dn
		 */
		require_once 'Zend/Ldap/Dn.php';
		return Zend_Ldap_Dn::checkDn($dn, $keys, $vals);
	}

	/**
	 * Constructor.
	 *
	 * @param  array|Zend_Config $options Options used in connecting, binding, etc.
	 * @return void
	 */
	public function __construct($options = array())
	{
		$this->setOptions($options);
	}

	/**
	 * Destructor.
	 *
	 * @return void
	 */
	public function __destruct()
	{
		$this->disconnect();
	}

	/**
	 * @return resource The raw LDAP extension resource.
	 */
	public function getResource()
	{
		return $this->_resource;
	}

	/**
	 * Return the LDAP error number of the last LDAP command
	 *
	 * @return int
	 */
	public function getLastErrorCode()
	{
		$ret = @ldap_get_option($this->getResource(), LDAP_OPT_ERROR_NUMBER, $err);
		if ($ret === true) {
			if ($err <= -1 && $err >= -17) {
				/**
				 * @see Zend_Ldap_Exception
				 */
				require_once 'Zend/Ldap/Exception.php';
				/* For some reason draft-ietf-ldapext-ldap-c-api-xx.txt error
				 * codes in OpenLDAP are negative values from -1 to -17.
				 */
				$err = Zend_Ldap_Exception::LDAP_SERVER_DOWN + (-$err - 1);
			}
			return $err;
		}
		return 0;
	}

	/**
	 * Return the LDAP error message of the last LDAP command
	 *
	 * @param  int   $errorCode
	 * @param  array $errorMessages
	 * @return string
	 */
	public function getLastError(&$errorCode = null, array &$errorMessages = null)
	{
		$errorCode = $this->getLastErrorCode();
		$errorMessages = array();

		/* The various error retrieval functions can return
		 * different things so we just try to collect what we
		 * can and eliminate dupes.
		 */
		$estr1 = @ldap_error($this->getResource());
		if ($errorCode !== 0 && $estr1 === 'Success') {
			$estr1 = @ldap_err2str($errorCode);
		}
		if (!empty($estr1)) {
			$errorMessages[] = $estr1;
		}

		@ldap_get_option($this->getResource(), LDAP_OPT_ERROR_STRING, $estr2);
		if (!empty($estr2) && !in_array($estr2, $errorMessages)) {
			$errorMessages[] = $estr2;
		}

		$message = '';
		if ($errorCode > 0) {
			$message = '0x' . dechex($errorCode) . ' ';
		} else {
			$message = '';
		}
		if (count($errorMessages) > 0) {
			$message .= '(' . implode('; ', $errorMessages) . ')';
		} else {
			$message .= '(no error message from LDAP)';
		}
		return $message;
	}

	/**
	 * Sets the options used in connecting, binding, etc.
	 *
	 * Valid option keys:
	 *  host
	 *  port
	 *  useSsl
	 *  username
	 *  password
	 *  bindRequiresDn
	 *  baseDn
	 *  accountCanonicalForm
	 *  accountDomainName
	 *  accountDomainNameShort
	 *  accountFilterFormat
	 *  allowEmptyPassword
	 *  useStartTls
	 *  optRefferals
	 *  tryUsernameSplit
	 *
	 * @param  array|Zend_Config $options Options used in connecting, binding, etc.
	 * @return Zend_Ldap Provides a fluent interface
	 * @throws Zend_Ldap_Exception
	 */
	public function setOptions($options)
	{
		if ($options instanceof Zend_Config) {
			$options = $options->toArray();
		}

		$permittedOptions = array(
            'host'                   => null,
            'port'                   => 0,
            'useSsl'                 => false,
            'username'               => null,
            'password'               => null,
            'bindRequiresDn'         => false,
            'baseDn'                 => null,
            'accountCanonicalForm'   => null,
            'accountDomainName'      => null,
            'accountDomainNameShort' => null,
            'accountFilterFormat'    => null,
            'allowEmptyPassword'     => false,
            'useStartTls'            => false,
            'optReferrals'           => false,
            'tryUsernameSplit'       => true,
		);

		foreach ($permittedOptions as $key => $val) {
			if (array_key_exists($key, $options)) {
				$val = $options[$key];
				unset($options[$key]);
				/* Enforce typing. This eliminates issues like Zend_Config_Ini
				 * returning '1' as a string (ZF-3163).
				 */
				switch ($key) {
					case 'port':
					case 'accountCanonicalForm':
						$permittedOptions[$key] = (int)$val;
						break;
					case 'useSsl':
					case 'bindRequiresDn':
					case 'allowEmptyPassword':
					case 'useStartTls':
					case 'optReferrals':
					case 'tryUsernameSplit':
						$permittedOptions[$key] = ($val === true ||
						$val === '1' || strcasecmp($val, 'true') == 0);
						break;
					default:
						$permittedOptions[$key] = trim($val);
						break;
				}
			}
		}
		if (count($options) > 0) {
			$key = key($options);
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			throw new Zend_Ldap_Exception(null, "Unknown Zend_Ldap option: $key");
		}
		$this->_options = $permittedOptions;
		return $this;
	}

	/**
	 * @return array The current options.
	 */
	public function getOptions()
	{
		return $this->_options;
	}

	/**
	 * @return string The hostname of the LDAP server being used to authenticate accounts
	 */
	protected function _getHost()
	{
		return $this->_options['host'];
	}

	/**
	 * @return int The port of the LDAP server or 0 to indicate that no port value is set
	 */
	protected function _getPort()
	{
		return $this->_options['port'];
	}

	/**
	 * @return boolean The default SSL / TLS encrypted transport control
	 */
	protected function _getUseSsl()
	{
		return $this->_options['useSsl'];
	}

	/**
	 * @return string The default acctname for binding
	 */
	protected function _getUsername()
	{
		return $this->_options['username'];
	}

	/**
	 * @return string The default password for binding
	 */
	protected function _getPassword()
	{
		return $this->_options['password'];
	}

	/**
	 * @return boolean Bind requires DN
	 */
	protected function _getBindRequiresDn()
	{
		return $this->_options['bindRequiresDn'];
	}

	/**
	 * Gets the base DN under which objects of interest are located
	 *
	 * @return string
	 */
	public function getBaseDn()
	{
		return $this->_options['baseDn'];
	}

	/**
	 * @return integer Either ACCTNAME_FORM_BACKSLASH, ACCTNAME_FORM_PRINCIPAL or
	 * ACCTNAME_FORM_USERNAME indicating the form usernames should be canonicalized to.
	 */
	protected function _getAccountCanonicalForm()
	{
		/* Account names should always be qualified with a domain. In some scenarios
		 * using non-qualified account names can lead to security vulnerabilities. If
		 * no account canonical form is specified, we guess based in what domain
		 * names have been supplied.
		 */

		$accountCanonicalForm = $this->_options['accountCanonicalForm'];
		if (!$accountCanonicalForm) {
			$accountDomainName = $this->_getAccountDomainName();
			$accountDomainNameShort = $this->_getAccountDomainNameShort();
			if ($accountDomainNameShort) {
				$accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_BACKSLASH;
			} else if ($accountDomainName) {
				$accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_PRINCIPAL;
			} else {
				$accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_USERNAME;
			}
		}

		return $accountCanonicalForm;
	}

	/**
	 * @return string The account domain name
	 */
	protected function _getAccountDomainName()
	{
		return $this->_options['accountDomainName'];
	}

	/**
	 * @return string The short account domain name
	 */
	protected function _getAccountDomainNameShort()
	{
		return $this->_options['accountDomainNameShort'];
	}

	/**
	 * @return string A format string for building an LDAP search filter to match
	 * an account
	 */
	protected function _getAccountFilterFormat()
	{
		return $this->_options['accountFilterFormat'];
	}

	/**
	 * @return boolean Allow empty passwords
	 */
	protected function _getAllowEmptyPassword()
	{
		return $this->_options['allowEmptyPassword'];
	}

	/**
	 * @return boolean The default SSL / TLS encrypted transport control
	 */
	protected function _getUseStartTls()
	{
		return $this->_options['useStartTls'];
	}

	/**
	 * @return boolean Opt. Referrals
	 */
	protected function _getOptReferrals()
	{
		return $this->_options['optReferrals'];
	}

	/**
	 * @return boolean Try splitting the username into username and domain
	 */
	protected function _getTryUsernameSplit()
	{
		return $this->_options['tryUsernameSplit'];
	}

	/**
	 * @return string The LDAP search filter for matching directory accounts
	 */
	protected function _getAccountFilter($acctname)
	{
		/**
		 * @see Zend_Ldap_Filter_Abstract
		 */
		require_once 'Zend/Ldap/Filter/Abstract.php';
		$this->_splitName($acctname, $dname, $aname);
		$accountFilterFormat = $this->_getAccountFilterFormat();
		$aname = Zend_Ldap_Filter_Abstract::escapeValue($aname);
		if ($accountFilterFormat) {
			return sprintf($accountFilterFormat, $aname);
		}
		if (!$this->_getBindRequiresDn()) {
			// is there a better way to detect this?
			return sprintf("(&(objectClass=user)(sAMAccountName=%s))", $aname);
		}
		return sprintf("(&(objectClass=posixAccount)(uid=%s))", $aname);
	}

	/**
	 * @param string $name  The name to split
	 * @param string $dname The resulting domain name (this is an out parameter)
	 * @param string $aname The resulting account name (this is an out parameter)
	 * @return void
	 */
	protected function _splitName($name, &$dname, &$aname)
	{
		$dname = null;
		$aname = $name;

		if (!$this->_getTryUsernameSplit()) {
			return;
		}

		$pos = strpos($name, '@');
		if ($pos) {
			$dname = substr($name, $pos + 1);
			$aname = substr($name, 0, $pos);
		} else {
			$pos = strpos($name, '\\');
			if ($pos) {
				$dname = substr($name, 0, $pos);
				$aname = substr($name, $pos + 1);
			}
		}
	}

	/**
	 * @param  string $acctname The name of the account
	 * @return string The DN of the specified account
	 * @throws Zend_Ldap_Exception
	 */
	protected function _getAccountDn($acctname)
	{
		/**
		 * @see Zend_Ldap_Dn
		 */
		require_once 'Zend/Ldap/Dn.php';
		if (Zend_Ldap_Dn::checkDn($acctname)) return $acctname;
		$acctname = $this->getCanonicalAccountName($acctname, Zend_Ldap::ACCTNAME_FORM_USERNAME);
		$acct = $this->_getAccount($acctname, array('dn'));
		return $acct['dn'];
	}

	/**
	 * @param  string $dname The domain name to check
	 * @return boolean
	 */
	protected function _isPossibleAuthority($dname)
	{
		if ($dname === null) {
			return true;
		}
		$accountDomainName = $this->_getAccountDomainName();
		$accountDomainNameShort = $this->_getAccountDomainNameShort();
		if ($accountDomainName === null && $accountDomainNameShort === null) {
			return true;
		}
		if (strcasecmp($dname, $accountDomainName) == 0) {
			return true;
		}
		if (strcasecmp($dname, $accountDomainNameShort) == 0) {
			return true;
		}
		return false;
	}

	/**
	 * @param  string $acctname The name to canonicalize
	 * @param  int    $type     The desired form of canonicalization
	 * @return string The canonicalized name in the desired form
	 * @throws Zend_Ldap_Exception
	 */
	public function getCanonicalAccountName($acctname, $form = 0)
	{
		$this->_splitName($acctname, $dname, $uname);

		if (!$this->_isPossibleAuthority($dname)) {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			throw new Zend_Ldap_Exception(null,
                "Binding domain is not an authority for user: $acctname",
			Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH);
		}

		if (!$uname) {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			throw new Zend_Ldap_Exception(null, "Invalid account name syntax: $acctname");
		}

		if (function_exists('mb_strtolower')) {
			$uname = mb_strtolower($uname, 'UTF-8');
		} else {
			$uname = strtolower($uname);
		}

		if ($form === 0) {
			$form = $this->_getAccountCanonicalForm();
		}

		switch ($form) {
			case Zend_Ldap::ACCTNAME_FORM_DN:
				return $this->_getAccountDn($acctname);
			case Zend_Ldap::ACCTNAME_FORM_USERNAME:
				return $uname;
			case Zend_Ldap::ACCTNAME_FORM_BACKSLASH:
				$accountDomainNameShort = $this->_getAccountDomainNameShort();
				if (!$accountDomainNameShort) {
					/**
					 * @see Zend_Ldap_Exception
					 */
					require_once 'Zend/Ldap/Exception.php';
					throw new Zend_Ldap_Exception(null, 'Option required: accountDomainNameShort');
				}
				return "$accountDomainNameShort\\$uname";
			case Zend_Ldap::ACCTNAME_FORM_PRINCIPAL:
				$accountDomainName = $this->_getAccountDomainName();
				if (!$accountDomainName) {
					/**
					 * @see Zend_Ldap_Exception
					 */
					require_once 'Zend/Ldap/Exception.php';
					throw new Zend_Ldap_Exception(null, 'Option required: accountDomainName');
				}
				return "$uname@$accountDomainName";
			default:
				/**
				 * @see Zend_Ldap_Exception
				 */
				require_once 'Zend/Ldap/Exception.php';
				throw new Zend_Ldap_Exception(null, "Unknown canonical name form: $form");
		}
	}

	/**
	 * @param  array $attrs An array of names of desired attributes
	 * @return array An array of the attributes representing the account
	 * @throws Zend_Ldap_Exception
	 */
	protected function _getAccount($acctname, array $attrs = null)
	{
		$baseDn = $this->getBaseDn();
		if (!$baseDn) {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			throw new Zend_Ldap_Exception(null, 'Base DN not set');
		}

		$accountFilter = $this->_getAccountFilter($acctname);
		if (!$accountFilter) {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			throw new Zend_Ldap_Exception(null, 'Invalid account filter');
		}

		if (!is_resource($this->getResource())) {
			$this->bind();
		}

		$accounts = $this->search($accountFilter, $baseDn, self::SEARCH_SCOPE_SUB, $attrs);
		$count = $accounts->count();
		if ($count === 1) {
			$acct = $accounts->getFirst();
			$accounts->close();
			return $acct;
		} else if ($count === 0) {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			$code = Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT;
			$str = "No object found for: $accountFilter";
		} else {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			$code = Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR;
			$str = "Unexpected result count ($count) for: $accountFilter";
		}
		$accounts->close();
		/**
		 * @see Zend_Ldap_Exception
		 */
		require_once 'Zend/Ldap/Exception.php';
		throw new Zend_Ldap_Exception($this, $str, $code);
	}

	/**
	 * @return Zend_Ldap Provides a fluent interface
	 */
	public function disconnect()
	{
		if (is_resource($this->getResource())) {
			if (!extension_loaded('ldap')) {
				/**
				 * @see Zend_Ldap_Exception
				 */
				require_once 'Zend/Ldap/Exception.php';
				throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded',
				Zend_Ldap_Exception::LDAP_X_EXTENSION_NOT_LOADED);
			}
			@ldap_unbind($this->getResource());
		}
		$this->_resource = null;
		return $this;
	}

	/**
	 * @param  string  $host        The hostname of the LDAP server to connect to
	 * @param  int     $port        The port number of the LDAP server to connect to
	 * @param  boolean $useSsl      Use SSL
	 * @param  boolean $useStartTls Use STARTTLS
	 * @return Zend_Ldap Provides a fluent interface
	 * @throws Zend_Ldap_Exception
	 */
	public function connect($host = null, $port = null, $useSsl = null, $useStartTls = null)
	{
		if ($host === null) {
			$host = $this->_getHost();
		}
		if ($port === null) {
			$port = $this->_getPort();
		} else {
			$port = (int)$port;
		}
		if ($useSsl === null) {
			$useSsl = $this->_getUseSsl();
		} else {
			$useSsl = (bool)$useSsl;
		}
		if ($useStartTls === null) {
			$useStartTls = $this->_getUseStartTls();
		} else {
			$useStartTls = (bool)$useStartTls;
		}

		if (!$host) {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			throw new Zend_Ldap_Exception(null, 'A host parameter is required');
		}

		/* To connect using SSL it seems the client tries to verify the server
		 * certificate by default. One way to disable this behavior is to set
		 * 'TLS_REQCERT never' in OpenLDAP's ldap.conf and restarting Apache. Or,
		 * if you really care about the server's cert you can put a cert on the
		 * web server.
		 */
		$url = ($useSsl) ? "ldaps://$host" : "ldap://$host";
		if ($port) {
			$url .= ":$port";
		}

		/* Because ldap_connect doesn't really try to connect, any connect error
		 * will actually occur during the ldap_bind call. Therefore, we save the
		 * connect string here for reporting it in error handling in bind().
		 */
		$this->_connectString = $url;

		$this->disconnect();

		if (!extension_loaded('ldap')) {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded',
			Zend_Ldap_Exception::LDAP_X_EXTENSION_NOT_LOADED);
		}

		/* Only OpenLDAP 2.2 + supports URLs so if SSL is not requested, just
		 * use the old form.
		 */
		$resource = ($useSsl) ? @ldap_connect($url) : @ldap_connect($host, $port);

		if (is_resource($resource) === true) {
			$this->_resource = $resource;

			$optReferrals = ($this->_getOptReferrals()) ? 1 : 0;
			if (@ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3) &&
			@ldap_set_option($resource, LDAP_OPT_REFERRALS, $optReferrals)) {
				if ($useSsl || !$useStartTls || @ldap_start_tls($resource)) {
					return $this;
				}
			}

			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			$zle = new Zend_Ldap_Exception($this, "$host:$port");
			$this->disconnect();
			throw $zle;
		}
		/**
		 * @see Zend_Ldap_Exception
		 */
		require_once 'Zend/Ldap/Exception.php';
		throw new Zend_Ldap_Exception(null, "Failed to connect to LDAP server: $host:$port");
	}

	/**
	 * @param  string $username The username for authenticating the bind
	 * @param  string $password The password for authenticating the bind
	 * @return Zend_Ldap Provides a fluent interface
	 * @throws Zend_Ldap_Exception
	 */
	public function bind($username = null, $password = null)
	{
		$moreCreds = true;

		if ($username === null) {
			$username = $this->_getUsername();
			$password = $this->_getPassword();
			$moreCreds = false;
		}

		if ($username === null) {
			/* Perform anonymous bind
			 */
			$password = null;
		} else {
			/* Check to make sure the username is in DN form.
			 */
			/**
			 * @see Zend_Ldap_Dn
			 */
			require_once 'Zend/Ldap/Dn.php';
			if (!Zend_Ldap_Dn::checkDn($username)) {
				if ($this->_getBindRequiresDn()) {
					/* moreCreds stops an infinite loop if _getUsername does not
					 * return a DN and the bind requires it
					 */
					if ($moreCreds) {
						try {
							$username = $this->_getAccountDn($username);
						} catch (Zend_Ldap_Exception $zle) {
							switch ($zle->getCode()) {
								case Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT:
								case Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH:
								case Zend_Ldap_Exception::LDAP_X_EXTENSION_NOT_LOADED:
									throw $zle;
							}
							throw new Zend_Ldap_Exception(null,
                                'Failed to retrieve DN for account: ' . $username .
                                ' [' . $zle->getMessage() . ']',
							Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR);
						}
					} else {
						/**
						 * @see Zend_Ldap_Exception
						 */
						require_once 'Zend/Ldap/Exception.php';
						throw new Zend_Ldap_Exception(null, 'Binding requires username in DN form');
					}
				} else {
					$username = $this->getCanonicalAccountName($username,
					$this->_getAccountCanonicalForm());
				}
			}
		}

		if (!is_resource($this->getResource())) {
			$this->connect();
		}

		if ($username !== null && $password === '' && $this->_getAllowEmptyPassword() !== true) {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			$zle = new Zend_Ldap_Exception(null,
                'Empty password not allowed - see allowEmptyPassword option.');
		} else {
			if (@ldap_bind($this->getResource(), $username, $password)) {
				return $this;
			}

			$message = ($username === null) ? $this->_connectString : $username;
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			switch ($this->getLastErrorCode()) {
				case Zend_Ldap_Exception::LDAP_SERVER_DOWN:
					/* If the error is related to establishing a connection rather than binding,
					 * the connect string is more informative than the username.
					 */
					$message = $this->_connectString;
			}

			$zle = new Zend_Ldap_Exception($this, $message);
		}
		$this->disconnect();
		throw $zle;
	}

	/**
	 * A global LDAP search routine for finding information.
	 *
	 * @param  string|Zend_Ldap_Filter_Abstract $filter
	 * @param  string|Zend_Ldap_Dn|null         $basedn
	 * @param  integer                          $scope
	 * @param  array                            $attributes
	 * @param  string|null                      $sort
	 * @param  string|null                      $collectionClass
	 * @return Zend_Ldap_Collection
	 * @throws Zend_Ldap_Exception
	 */
	public function search($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB,
	array $attributes = array(), $sort = null, $collectionClass = null)
	{
		if ($basedn === null) {
			$basedn = $this->getBaseDn();
		}
		else if ($basedn instanceof Zend_Ldap_Dn) {
			$basedn = $basedn->toString();
		}

		if ($filter instanceof Zend_Ldap_Filter_Abstract) {
			$filter = $filter->toString();
		}

		switch ($scope) {
			case self::SEARCH_SCOPE_ONE:
				$search = @ldap_list($this->getResource(), $basedn, $filter, $attributes);
				break;
			case self::SEARCH_SCOPE_BASE:
				$search = @ldap_read($this->getResource(), $basedn, $filter, $attributes);
				break;
			case self::SEARCH_SCOPE_SUB:
			default:
				$search = @ldap_search($this->getResource(), $basedn, $filter, $attributes);
				break;
		}

		if($search === false) {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			throw new Zend_Ldap_Exception($this, 'searching: ' . $filter);
		}
		if (!is_null($sort) && is_string($sort)) {
			$isSorted = @ldap_sort($this->getResource(), $search, $sort);
			if($search === false) {
				/**
				 * @see Zend_Ldap_Exception
				 */
				require_once 'Zend/Ldap/Exception.php';
				throw new Zend_Ldap_Exception($this, 'sorting: ' . $sort);
			}
		}

		/**
		 * Zend_Ldap_Collection_Iterator_Default
		 */
		require_once 'Zend/Ldap/Collection/Iterator/Default.php';
		$iterator = new Zend_Ldap_Collection_Iterator_Default($this, $search);
		if ($collectionClass === null) {
			/**
			 * Zend_Ldap_Collection
			 */
			require_once 'Zend/Ldap/Collection.php';
			return new Zend_Ldap_Collection($iterator);
		} else {
			$collectionClass = (string)$collectionClass;
			if (!class_exists($collectionClass)) {
				/**
				 * @see Zend_Ldap_Exception
				 */
				require_once 'Zend/Ldap/Exception.php';
				throw new Zend_Ldap_Exception(null,
                    "Class '$collectionClass' can not be found");
			}
			if (!is_subclass_of($collectionClass, 'Zend_Ldap_Collection')) {
				/**
				 * @see Zend_Ldap_Exception
				 */
				require_once 'Zend/Ldap/Exception.php';
				throw new Zend_Ldap_Exception(null,
                    "Class '$collectionClass' must subclass 'Zend_Ldap_Collection'");
			}
			return new $collectionClass($iterator);
		}
	}

	/**
	 * Count items found by given filter.
	 *
	 * @param  string|Zend_Ldap_Filter_Abstract $filter
	 * @param  string|Zend_Ldap_Dn|null         $basedn
	 * @param  integer                          $scope
	 * @return integer
	 * @throws Zend_Ldap_Exception
	 */
	public function count($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB)
	{
		try {
			$result = $this->search($filter, $basedn, $scope, array('dn'), null);
		} catch (Zend_Ldap_Exception $e) {
			if ($e->getCode() === Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT) return 0;
			else throw $e;
		}
		return $result->count();
	}

	/**
	 * Count children for a given DN.
	 *
	 * @param  string|Zend_Ldap_Dn $dn
	 * @return integer
	 * @throws Zend_Ldap_Exception
	 */
	public function countChildren($dn)
	{
		return $this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_ONE);
	}

	/**
	 * Check if a given DN exists.
	 *
	 * @param  string|Zend_Ldap_Dn $dn
	 * @return boolean
	 * @throws Zend_Ldap_Exception
	 */
	public function exists($dn)
	{
		return ($this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_BASE) == 1);
	}

	/**
	 * Search LDAP registry for entries matching filter and optional attributes
	 *
	 * @param  string|Zend_Ldap_Filter_Abstract $filter
	 * @param  string|Zend_Ldap_Dn|null         $basedn
	 * @param  integer                          $scope
	 * @param  array                            $attributes
	 * @param  string|null                      $sort
	 * @return array
	 * @throws Zend_Ldap_Exception
	 */
	public function searchEntries($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB,
	array $attributes = array(), $sort = null)
	{
		$result = $this->search($filter, $basedn, $scope, $attributes, $sort);
		return $result->toArray();
	}

	/**
	 * Get LDAP entry by DN
	 *
	 * @param  string|Zend_Ldap_Dn $dn
	 * @param  array               $attributes
	 * @param  boolean             $throwOnNotFound
	 * @return array
	 * @throws Zend_Ldap_Exception
	 */
	public function getEntry($dn, array $attributes = array(), $throwOnNotFound = false)
	{
		try {
			$result = $this->search("(objectClass=*)", $dn, self::SEARCH_SCOPE_BASE,
			$attributes, null);
			return $result->getFirst();
		} catch (Zend_Ldap_Exception $e){
			if ($throwOnNotFound !== false) throw $e;
		}
		return null;
	}

	/**
	 * Prepares an ldap data entry array for insert/update operation
	 *
	 * @param  array $entry
	 * @return void
	 * @throws InvalidArgumentException
	 */
	public static function prepareLdapEntryArray(array &$entry)
	{
		if (array_key_exists('dn', $entry)) unset($entry['dn']);
		foreach ($entry as $key => $value) {
			if (is_array($value)) {
				foreach ($value as $i => $v) {
					if (is_null($v)) unset($value[$i]);
					else if (!is_scalar($v)) {
						throw new InvalidArgumentException('Only scalar values allowed in LDAP data');
					} else {
						$v = (string)$v;
						if (strlen($v) == 0) {
							unset($value[$i]);
						} else {
							$value[$i] = $v;
						}
					}
				}
				$entry[$key] = array_values($value);
			} else {
				if (is_null($value)) $entry[$key] = array();
				else if (!is_scalar($value)) {
					throw new InvalidArgumentException('Only scalar values allowed in LDAP data');
				} else {
					$value = (string)$value;
					if (strlen($value) == 0) {
						$entry[$key] = array();
					} else {
						$entry[$key] = array($value);
					}
				}
			}
		}
		$entry = array_change_key_case($entry, CASE_LOWER);
	}

	/**
	 * Add new information to the LDAP repository
	 *
	 * @param  string|Zend_Ldap_Dn $dn
	 * @param  array               $entry
	 * @return Zend_Ldap                  Provides a fluid interface
	 * @throws Zend_Ldap_Exception
	 */
	public function add($dn, array $entry)
	{
		if (!($dn instanceof Zend_Ldap_Dn)) {
			$dn = Zend_Ldap_Dn::factory($dn, null);
		}
		self::prepareLdapEntryArray($entry);
		foreach ($entry as $key => $value) {
			if (is_array($value) && count($value) === 0) {
				unset($entry[$key]);
			}
		}

		$rdnParts = $dn->getRdn(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
		foreach ($rdnParts as $key => $value) {
			$value = Zend_Ldap_Dn::unescapeValue($value);
			if (!array_key_exists($key, $entry) ||
			!in_array($value, $entry[$key]) ||
			count($entry[$key]) !== 1) {
				$entry[$key] = array($value);
			}
		}
		$adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory',
            'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated');
		foreach ($adAttributes as $attr) {
			if (array_key_exists($attr, $entry)) {
				unset($entry[$attr]);
			}
		}

		$isAdded = @ldap_add($this->getResource(), $dn->toString(), $entry);
		if($isAdded === false) {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			throw new Zend_Ldap_Exception($this, 'adding: ' . $dn->toString());
		}
		return $this;
	}

	/**
	 * Update LDAP registry
	 *
	 * @param  string|Zend_Ldap_Dn $dn
	 * @param  array               $entry
	 * @return Zend_Ldap                  Provides a fluid interface
	 * @throws Zend_Ldap_Exception
	 */
	public function update($dn, array $entry)
	{
		if (!($dn instanceof Zend_Ldap_Dn)) {
			$dn = Zend_Ldap_Dn::factory($dn, null);
		}
		self::prepareLdapEntryArray($entry);

		$rdnParts = $dn->getRdn(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
		$adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory',
            'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated');
		$stripAttributes = array_merge(array_keys($rdnParts), $adAttributes);
		foreach ($stripAttributes as $attr) {
			if (array_key_exists($attr, $entry)) {
				unset($entry[$attr]);
			}
		}

		if (count($entry) > 0) {
			$isModified = @ldap_modify($this->getResource(), $dn->toString(), $entry);
			if($isModified === false) {
				/**
				 * @see Zend_Ldap_Exception
				 */
				require_once 'Zend/Ldap/Exception.php';
				throw new Zend_Ldap_Exception($this, 'updating: ' . $dn->toString());
			}
		}
		return $this;
	}

	/**
	 * Save entry to LDAP registry.
	 *
	 * Internally decides if entry will be updated to added by calling
	 * {@link exists()}.
	 *
	 * @param  string|Zend_Ldap_Dn $dn
	 * @param  array               $entry
	 * @return Zend_Ldap Provides a fluid interface
	 * @throws Zend_Ldap_Exception
	 */
	public function save($dn, array $entry)
	{
		if ($dn instanceof Zend_Ldap_Dn) {
			$dn = $dn->toString();
		}
		if ($this->exists($dn)) $this->update($dn, $entry);
		else $this->add($dn, $entry);
		return $this;
	}

	/**
	 * Delete an LDAP entry
	 *
	 * @param  string|Zend_Ldap_Dn $dn
	 * @param  boolean             $recursively
	 * @return Zend_Ldap Provides a fluid interface
	 * @throws Zend_Ldap_Exception
	 */
	public function delete($dn, $recursively = false)
	{
		if ($dn instanceof Zend_Ldap_Dn) {
			$dn = $dn->toString();
		}
		if ($recursively === true) {
			if ($this->countChildren($dn)>0) {
				$children = $this->_getChildrenDns($dn);
				foreach ($children as $c) {
					$this->delete($c, true);
				}
			}
		}
		$isDeleted = @ldap_delete($this->getResource(), $dn);
		if($isDeleted === false) {
			/**
			 * @see Zend_Ldap_Exception
			 */
			require_once 'Zend/Ldap/Exception.php';
			throw new Zend_Ldap_Exception($this, 'deleting: ' . $dn);
		}
		return $this;
	}

	/**
	 * Retrieve the immediate children DNs of the given $parentDn
	 *
	 * This method is used in recursive methods like {@see delete()}
	 * or {@see copy()}
	 *
	 * @param  string|Zend_Ldap_Dn $parentDn
	 * @return array of DNs
	 */
	protected function _getChildrenDns($parentDn)
	{
		if ($parentDn instanceof Zend_Ldap_Dn) {
			$parentDn = $parentDn->toString();
		}
		$children = array();
		$search = @ldap_list($this->getResource(), $parentDn, '(objectClass=*)', array('dn'));
		for ($entry = @ldap_first_entry($this->getResource(), $search);
		$entry !== false;
		$entry = @ldap_next_entry($this->getResource(), $entry)) {
			$childDn = @ldap_get_dn($this->getResource(), $entry);
			if ($childDn === false) {
				/**
				 * @see Zend_Ldap_Exception
				 */
				require_once 'Zend/Ldap/Exception.php';
				throw new Zend_Ldap_Exception($this, 'getting dn');
			}
			$children[] = $childDn;
		}
		@ldap_free_result($search);
		return $children;
	}

	/**
	 * Moves a LDAP entry from one DN to another subtree.
	 *
	 * @param  string|Zend_Ldap_Dn $from
	 * @param  string|Zend_Ldap_Dn $to
	 * @param  boolean             $recursively
	 * @param  boolean             $alwaysEmulate
	 * @return Zend_Ldap Provides a fluid interface
	 * @throws Zend_Ldap_Exception
	 */
	public function moveToSubtree($from, $to, $recursively = false, $alwaysEmulate = false)
	{
		if ($from instanceof Zend_Ldap_Dn) {
			$orgDnParts = $from->toArray();
		} else {
			$orgDnParts = Zend_Ldap_Dn::explodeDn($from);
		}

		if ($to instanceof Zend_Ldap_Dn) {
			$newParentDnParts = $to->toArray();
		} else {
			$newParentDnParts = Zend_Ldap_Dn::explodeDn($to);
		}

		$newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts);
		$newDn = Zend_Ldap_Dn::fromArray($newDnParts);
		return $this->rename($from, $newDn, $recursively, $alwaysEmulate);
	}

	/**
	 * Moves a LDAP entry from one DN to another DN.
	 *
	 * This is an alias for {@link rename()}
	 *
	 * @param  string|Zend_Ldap_Dn $from
	 * @param  string|Zend_Ldap_Dn $to
	 * @param  boolean             $recursively
	 * @param  boolean             $alwaysEmulate
	 * @return Zend_Ldap Provides a fluid interface
	 * @throws Zend_Ldap_Exception
	 */
	public function move($from, $to, $recursively = false, $alwaysEmulate = false)
	{
		return $this->rename($from, $to, $recursively, $alwaysEmulate);
	}

	/**
	 * Renames a LDAP entry from one DN to another DN.
	 *
	 * This method implicitely moves the entry to another location within the tree.
	 *
	 * @param  string|Zend_Ldap_Dn $from
	 * @param  string|Zend_Ldap_Dn $to
	 * @param  boolean             $recursively
	 * @param  boolean             $alwaysEmulate
	 * @return Zend_Ldap Provides a fluid interface
	 * @throws Zend_Ldap_Exception
	 */
	public function rename($from, $to, $recursively = false, $alwaysEmulate = false)
	{
		$emulate = (bool)$alwaysEmulate;
		if (!function_exists('ldap_rename')) $emulate = true;
		else if ($recursively) $emulate = true;

		if ($emulate === false) {
			if ($from instanceof Zend_Ldap_Dn) {
				$from = $from->toString();
			}

			if ($to instanceof Zend_Ldap_Dn) {
				$newDnParts = $to->toArray();
			} else {
				$newDnParts = Zend_Ldap_Dn::explodeDn($to);
			}

			$newRdn = Zend_Ldap_Dn::implodeRdn(array_shift($newDnParts));
			$newParent = Zend_Ldap_Dn::implodeDn($newDnParts);
			$isOK = @ldap_rename($this->getResource(), $from, $newRdn, $newParent, true);
			if($isOK === false) {
				/**
				 * @see Zend_Ldap_Exception
				 */
				require_once 'Zend/Ldap/Exception.php';
				throw new Zend_Ldap_Exception($this, 'renaming ' . $from . ' to ' . $to);
			}
			else if (!$this->exists($to)) $emulate = true;
		}
		if ($emulate) {
			$this->copy($from, $to, $recursively);
			$this->delete($from, $recursively);
		}
		return $this;
	}

	/**
	 * Copies a LDAP entry from one DN to another subtree.
	 *
	 * @param  string|Zend_Ldap_Dn $from
	 * @param  string|Zend_Ldap_Dn $to
	 * @param  boolean             $recursively
	 * @return Zend_Ldap Provides a fluid interface
	 * @throws Zend_Ldap_Exception
	 */
	public function copyToSubtree($from, $to, $recursively = false)
	{
		if ($from instanceof Zend_Ldap_Dn) {
			$orgDnParts = $from->toArray();
		} else {
			$orgDnParts = Zend_Ldap_Dn::explodeDn($from);
		}

		if ($to instanceof Zend_Ldap_Dn) {
			$newParentDnParts = $to->toArray();
		} else {
			$newParentDnParts = Zend_Ldap_Dn::explodeDn($to);
		}

		$newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts);
		$newDn = Zend_Ldap_Dn::fromArray($newDnParts);
		return $this->copy($from, $newDn, $recursively);
	}

	/**
	 * Copies a LDAP entry from one DN to another DN.
	 *
	 * @param  string|Zend_Ldap_Dn $from
	 * @param  string|Zend_Ldap_Dn $to
	 * @param  boolean             $recursively
	 * @return Zend_Ldap Provides a fluid interface
	 * @throws Zend_Ldap_Exception
	 */
	public function copy($from, $to, $recursively = false)
	{
		$entry = $this->getEntry($from, array(), true);

		if ($to instanceof Zend_Ldap_Dn) {
			$toDnParts = $to->toArray();
		} else {
			$toDnParts = Zend_Ldap_Dn::explodeDn($to);
		}
		$this->add($to, $entry);

		if ($recursively === true && $this->countChildren($from)>0) {
			$children = $this->_getChildrenDns($from);
			foreach ($children as $c) {
				$cDnParts = Zend_Ldap_Dn::explodeDn($c);
				$newChildParts = array_merge(array(array_shift($cDnParts)), $toDnParts);
				$newChild = Zend_Ldap_Dn::implodeDn($newChildParts);
				$this->copy($c, $newChild, true);
			}
		}
		return $this;
	}

	/**
	 * Returns the specified DN as a Zend_Ldap_Node
	 *
	 * @param  string|Zend_Ldap_Dn $dn
	 * @return Zend_Ldap_Node|null
	 * @throws Zend_Ldap_Exception
	 */
	public function getNode($dn)
	{
		/**
		 * Zend_Ldap_Node
		 */
		require_once 'Zend/Ldap/Node.php';
		return Zend_Ldap_Node::fromLdap($dn, $this);
	}

	/**
	 * Returns the base node as a Zend_Ldap_Node
	 *
	 * @return Zend_Ldap_Node
	 * @throws Zend_Ldap_Exception
	 */
	public function getBaseNode()
	{
		return $this->getNode($this->getBaseDn(), $this);
	}

	/**
	 * Returns the RootDSE
	 *
	 * @return Zend_Ldap_Node_RootDse
	 * @throws Zend_Ldap_Exception
	 */
	public function getRootDse()
	{
		if ($this->_rootDse === null) {
			/**
			 * @see Zend_Ldap_Node_Schema
			 */
			require_once 'Zend/Ldap/Node/RootDse.php';
			$this->_rootDse = Zend_Ldap_Node_RootDse::create($this);
		}
		return $this->_rootDse;
	}

	/**
	 * Returns the schema
	 *
	 * @return Zend_Ldap_Node_Schema
	 * @throws Zend_Ldap_Exception
	 */
	public function getSchema()
	{
		if ($this->_schema === null) {
			/**
			 * @see Zend_Ldap_Node_Schema
			 */
			require_once 'Zend/Ldap/Node/Schema.php';
			$this->_schema = Zend_Ldap_Node_Schema::create($this);
		}
		return $this->_schema;
	}
}
